/*----------------------------------------------------------------------------
	Subprocess.m
	
	From Subprocess example by Charles L. Oei
									pty support by Joe Freeman
									with encouragement from Kristofer Younger
									Subprocess Example, Release 2.0
									NeXT Computer, Inc.
	
	You may freely copy, distribute and reuse the code in this example.
	NeXT disclaims any warranty of any kind, expressed or implied, as to
	its fitness for any particular use.
		
	SYNOPSIS
		Handles a UNIX process that runs asynchronously.
  
	REVISIONS
	Subprocess.m,v
# Revision 1.4  1993/02/01  02:21:31  nwc
# Added baud rate button. Cleaned interface. Fixed subprocess bug.
#
# Revision 1.3  1992/09/15  14:59:23  nwc
# Made NeXTSTEP 3.0 compatible.
# Added baud rate preference since that now works in uuq.
#
# Revision 1.2  1992/09/03  16:33:48  nwc
# Clean up after Subprocesses.
#
# Revision 1.1.1.1  1992/08/18  14:34:20  nwc
# GENESIS
#
# Revision 1.1  1992/07/04  03:17:22  nwc
# Initial revision
#
----------------------------------------------------------------------------*/
#import <sys/wait.h>
#import <sys/resource.h>
#import <appkit/nextstd.h>
#import <appkit/Application.h>
#import <appkit/Panel.h>
#import "Subprocess.h"

extern int  wait4(int, union wait *, int, struct rusage *);
static void fdHandler(int theFd, id self);
static void stderrFdHandler(int theFd, id self);

#define PIPE_ERROR			"Error starting UNIX pipes to subprocess."
#define VFORK_ERROR			"Error starting UNIX vfork of subprocess."

@interface Subprocess(Private)
- childDidExit;
- fdHandler:(int)theFd;
@end

@implementation Subprocess(Private)

/*
 * cleanup after a child process exits
 */
- childDidExit
{
   union wait  w;
   int         status = 0;
   int         thePid;

   thePid = wait4(childPid, &w, WUNTRACED, NULL);
#ifdef DEBUG
   fprintf(stderr, "%s: wait4() on process id #%d returned %d, with "
	   "w_status = %d, w_retcode = %u, w_stopval = %u, "
	   "w_stopsig = %u, w_termsig = %d\n",
	   [self name], childPid, thePid, w.w_status, w.w_retcode,
	   w.w_stopval, w.w_stopsig, w.w_termsig);
#endif
   if (thePid > 0)
   {
      if (WIFEXITED(w))
	 status = (w.w_status >> 8);
      else
      {
	 if (WIFSTOPPED(w))
	    status = SUBPROCESS_STOPPED;
	 else
	 {
	    if (WIFSIGNALED(w))
	       status = SUBPROCESS_SIGNALED;
	 }
      }
      DPSRemoveFD(fromChild);
      DPSRemoveFD(stderrFromChild);
      fclose(fpFromChild);
      close(fromChild);
      close(stderrFromChild);
      fclose(fpToChild);
      running = NO;
      [delegate perform:@selector(subprocess:done:)
       with :self
       with:(void *)status];
      if(markedForFree)
	  [NXApp delayedFree: self];
   }
   return (self);
}

/*
 * DPS handler for output from subprocess
 */
- fdHandler:(int)theFd
{
   char       *s, *linep;
   int         bufferCount;

   bufferCount = read(theFd, outputBuffer + outputBufferLen,
		      BUFSIZ - outputBufferLen);
   if (bufferCount <= 0)
   {
      [self childDidExit];
      return (self);
   }
   outputBuffer[bufferCount + outputBufferLen] = '\0';

   /*
    * Send lines in the buffer to the delegate 
    */
   s = linep = outputBuffer;
   while (s != NULL)
   {
      if ((s = index(linep, '\n')) != NULL)
      {
	 *s = (char)0;
	 [delegate perform:@selector(subprocess:output:)
	  with :self
	  with:(void *)linep];
	 linep = s + 1;
      }
   }

   /*
    * Copy the last part of the line back into the input buffer for next time (incomplete line) 
    */
   outputBufferLen = strlen(linep);
   strncpy(outputBuffer, linep, outputBufferLen);

   return (self);
}

@end

@implementation Subprocess

/*
 * Cover for the init:withDelegate: with a nil delegate
 */
- init:(const char *)subprocessString
{
   return ([self init:subprocessString
	    withDelegate:nil]);
}

- init:(const char *)subprocessString withDelegate:theDelegate
{
   int         pipeTo[2];
   int         pipeFrom[2];
   int         pipeStderr[2];		    /* for stderr to different fd */
   int         numFds, fd;
   int processGroup;

   [super init];
   markedForFree = NO;
   outputBufferLen = 0;
   stderrBufferLen = 0;
   [self setDelegate:theDelegate];

   if (pipe(pipeTo) < 0 || pipe(pipeFrom) < 0 || pipe(pipeStderr) < 0)
   {
      [delegate perform:@selector(subprocess:error:)
       with :self
       with:(void *)PIPE_ERROR];
      return (self);
   }

   switch (childPid = vfork())
   {
      case -1:				    /* error */
	 [delegate perform:@selector(subprocess:error:)
	  with :self
	  with:(void *)VFORK_ERROR];
	 return (self);

      case 0:				    /* child */
	 dup2(pipeTo[0], 0);
	 dup2(pipeFrom[1], 1);		    /* get stdout from process */
	 dup2(pipeStderr[1], 2);	    /* get stderr here */
	 numFds = getdtablesize();
	 for (fd = 3; fd < numFds; fd++)
	    close(fd);

	 processGroup = getpid();
	 ioctl(0, TIOCSPGRP, (char *)&processGroup);
	 setpgrp(0,processGroup);

	 /*
	  * we exec a /bin/sh so that cmds are easier to specify for the user 
	  */
	 execl("/bin/sh", "sh", "-c", subprocessString, 0);
	 perror("vfork (child)");	    /* should never gets here tho */
	 exit(1);

      default:				    /* parent */
	 running = YES;

	 close(pipeTo[0]);
	 close(pipeFrom[1]);
	 close(pipeStderr[1]);

	 fpToChild = fdopen(pipeTo[1], "w");
	 fromChild = pipeFrom[0];
	 fpFromChild = fdopen(pipeFrom[0], "r");

	 stderrFromChild = pipeStderr[0];

	 /*
	  * Set buffering method, also make it use its own buffers 
	  */
	 setbuf(fpToChild, NULL);	    /* no buffering */
	 setbuf(fpFromChild, NULL);
	 DPSAddFD(fromChild, (DPSFDProc) fdHandler, (id) self,
		  NX_MODALRESPTHRESHOLD + 1);

	 DPSAddFD(stderrFromChild, (DPSFDProc) stderrFdHandler, (id) self,
		  NX_MODALRESPTHRESHOLD + 1);

	 return (self);
   }
}


- stderrFdHandler:(int)theFd
{
   char       *s, *linep;
   int         bufferCount;

   bufferCount = read(theFd, stderrBuffer + stderrBufferLen,
		      BUFSIZ - stderrBufferLen);
   if (bufferCount <= 0)
      return (self);

   stderrBuffer[bufferCount + stderrBufferLen] = '\0';

   /*
    * Send lines in the buffer to the delegate 
    */
   s = linep = stderrBuffer;
   while (s != NULL)
   {
      if ((s = index(linep, '\n')) != NULL)
      {
	 *s = (char)0;
	 [delegate perform:@selector(subprocess:stderrOutput:)
	  with :self
	  with:(void *)linep];
	 linep = s + 1;
      }
   }

   /*
    * Copy the last part of the line back into the input buffer for next time (incomplete line) 
    */
   stderrBufferLen = strlen(linep);
   strncpy(stderrBuffer, linep, stderrBufferLen);

   return (self);
}

- send:(const char *)string withNewline:(BOOL) wantNewline
{
   fputs(string, fpToChild);
   if (wantNewline)
      fputc('\n', fpToChild);
   return (self);
}

- send:(const char *)string
{
   [self send:string withNewline:YES];
   return (self);
}

/*
 * Returns the process id of the process (and therefore the process group
 * of the job)
 */
- (int)pid
{
   return (childPid);
}

- (BOOL) isPaused
{
   return (paused);
}

- resume:sender
{
   if (paused)
   {
      killpg(childPid, SIGCONT);	    /* resume the process group */
      paused = NO;
   }
   return (self);
}

- pause:sender
{
   if (!paused)
   {
      killpg(childPid, SIGSTOP);	    /* pause the process group */
      paused = YES;
   }
   return (self);
}

- (BOOL) isRunning
{
   return (running);
}

- terminate:sender
{
   if (running)
      killpg(childPid, SIGKILL);
   return (self);
}

- free
{
   if(running && !markedForFree)
   {
      markedForFree = YES;
      return [self terminate: self];
   }
   else
       return [super free];
}

/*
 * effectively sends an EOF to the child process stdin
 */
- terminateInput
{
   fclose(fpToChild);
   return (self);
}

- setDelegate:anObject
{
   delegate = anObject;
   return (self);
}

- delegate
{
   return (delegate);
}

@end

@implementation Object(SubprocessDelegate)

- subprocess: sender done:(int)exitStatus
{
   return (self);
}

- subprocess:sender output:(char *)buffer
{
   return (self);
}

- subprocess:sender stderrOutput:(char *)buffer
{
   return (self);
}

- subprocess:sender error:(const char *)errorString
{
   if (NXApp)
      NXRunAlertPanel(0, errorString, 0, 0, 0);
   else
      perror(errorString);
   return (self);
}

@end


typedef struct
{
   @defs(Object)
} object;

/*
 * And standard error from subprocess
 */
static void stderrFdHandler(int theFd, id self)
{
   if(self && ((object *)self)->isa)
       [self stderrFdHandler:theFd];
}

/*
 * DPS handler for output from subprocess
 */
static void fdHandler(int theFd, id self)
{
   if(self && ((object *)self)->isa)
       [self fdHandler:theFd];
}


